跳到主要内容

MySQL 的 MVCC 是什么?

MVCC 基础概念

MVCC(Multi-Version Concurrency Control)多版本并发控制是一种数据库并发控制技术,通过保存数据的多个版本来实现读写操作的并发执行,避免了传统锁机制的性能瓶颈。

MVCC 的工作原理

MVCC 的核心思想是为每个数据行维护多个版本,每个事务都能看到数据在某个时间点的一致性快照。当事务读取数据时,系统会根据事务的可见性规则选择合适的数据版本,而不需要等待写操作完成。

MVCC 的关键特点:

  • 非阻塞读取:读操作永远不会被写操作阻塞
  • 版本链管理:通过 undo log 维护数据的历史版本
  • 可见性判断:通过 ReadView 机制判断数据版本的可见性
  • 事务隔离:为不同隔离级别提供实现基础

MVCC 工作的事务隔离级别

MVCC 主要工作在以下两个隔离级别:

READ COMMITTED(读已提交)

  • 每次查询都会创建新的 ReadView
  • 能够读取到其他事务已提交的最新数据
  • 可能出现不可重复读现象

REPEATABLE READ(可重复读)

  • 事务内第一次查询时创建 ReadView,后续查询复用
  • 保证事务内多次读取结果一致
  • MySQL InnoDB 的默认隔离级别

ReadView 可见性判断流程

银行转账并发控制时序图

MVCC 的核心组件

MVCC 解决的问题:

  • 读写并发: 读操作不阻塞写操作,写操作不阻塞读操作
  • 一致性读: 同一事务内多次读取结果一致
  • 隔离级别: 不同隔离级别的实现基础

隐藏列详解

InnoDB 为每个数据行添加三个隐藏列来支持 MVCC:

-- 实际存储结构示例
-- 用户表定义
CREATE TABLE account (
id VARCHAR(10) PRIMARY KEY,
balance DECIMAL(10,2)
);

-- 内部实际存储(包含隐藏列)
-- | id | balance | trx_id | roll_ptr | row_id |
-- |----|---------|---------|---------|---------|
-- | A | 1000.00 | 100 | 0x123 | NULL |
  • trx_id: 最后修改该行的事务ID
  • roll_ptr: 指向该行在 undo log 中的记录
  • row_id: 当表没有主键时的隐藏主键(有主键时为 NULL)

ReadView 可见性判断算法

MVCC 实际工作示例

银行转账的 MVCC 工作过程

-- 初始数据
-- account 表:
-- | id | balance | trx_id | roll_ptr |
-- |----|---------|--------|----------|
-- | A | 1000.00 | 99 | NULL |
-- | B | 500.00 | 99 | NULL |

-- 时间线演示
-- T1: 事务1开始 (id=100)
BEGIN;

-- T2: 事务2开始,立即查询 (id=101) - 创建 ReadView
BEGIN;
-- ReadView: {min_trx_id: 100, max_trx_id: 102, active_list: [100], creator: 101}
SELECT SUM(balance) FROM account WHERE id IN ('A', 'B');
-- 结果:1500.00 (看到的是 trx_id=99 的版本)

-- T3: 事务1执行转账
UPDATE account SET balance = 800.00 WHERE id = 'A'; -- trx_id=100
UPDATE account SET balance = 700.00 WHERE id = 'B'; -- trx_id=100

-- 此时数据状态:
-- | id | balance | trx_id | roll_ptr |
-- |----|---------|--------|--------------|
-- | A | 800.00 | 100 | undo_log_A1 |
-- | B | 700.00 | 100 | undo_log_B1 |

-- undo log 中保存了原始值:
-- undo_log_A1: {balance: 1000.00, trx_id: 99}
-- undo_log_B1: {balance: 500.00, trx_id: 99}

-- T4: 事务2再次查询(使用同一个 ReadView)
SELECT SUM(balance) FROM account WHERE id IN ('A', 'B');
-- 可见性判断:
-- 对于 A 行:trx_id=100 在 active_list 中 → 不可见 → 查找 undo log → 返回 1000.00
-- 对于 B 行:trx_id=100 在 active_list 中 → 不可见 → 查找 undo log → 返回 500.00
-- 结果:1500.00 (仍然是转账前的状态)

-- T5: 事务1提交
COMMIT;

-- T6: 事务2继续查询(仍使用同一个 ReadView)
SELECT SUM(balance) FROM account WHERE id IN ('A', 'B');
-- 结果:1500.00 (REPEATABLE READ 保证一致性)

-- T7: 事务2提交
COMMIT;

-- T8: 新事务3查询 (id=102)
BEGIN;
-- 新的 ReadView: {min_trx_id: 102, max_trx_id: 103, active_list: [], creator: 102}
SELECT SUM(balance) FROM account WHERE id IN ('A', 'B');
-- 可见性判断:
-- 对于 A 行:trx_id=100 < min_trx_id=102 → 可见 → 返回 800.00
-- 对于 B 行:trx_id=100 < min_trx_id=102 → 可见 → 返回 700.00
-- 结果:1500.00 (看到转账后的状态)

不同隔离级别的 ReadView 创建时机

-- READ COMMITTED 示例
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

BEGIN; -- 事务开始,但不创建 ReadView

-- 第一次查询,创建 ReadView1
SELECT * FROM account WHERE id = 'A';

-- 其他事务提交了修改...

-- 第二次查询,创建新的 ReadView2
SELECT * FROM account WHERE id = 'A'; -- 可能看到不同的结果

COMMIT;

-- REPEATABLE READ 示例
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

BEGIN; -- 事务开始,但不创建 ReadView

-- 第一次查询,创建 ReadView
SELECT * FROM account WHERE id = 'A';

-- 其他事务提交了修改...

-- 第二次查询,复用相同的 ReadView
SELECT * FROM account WHERE id = 'A'; -- 结果与第一次相同

COMMIT;

常见的 MVCC 相关问题

何时不使用 MVCC?

-- 以下查询会跳过 MVCC,直接读取最新版本
SELECT * FROM account WHERE id = 'A' FOR UPDATE; -- 当前读
SELECT * FROM account WHERE id = 'A' LOCK IN SHARE MODE; -- 当前读

-- DDL 操作不支持 MVCC
ALTER TABLE account ADD COLUMN email VARCHAR(100);

-- 在 READ UNCOMMITTED 隔离级别下不使用 MVCC
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

MVCC 的性能考虑:

-- 长事务会导致大量的 undo log 累积
BEGIN;
SELECT * FROM large_table LIMIT 1; -- 创建 ReadView
-- ... 长时间不提交,阻止 undo log 清理

-- 查看当前活跃事务
SELECT * FROM information_schema.INNODB_TRX;

-- 查看 undo log 使用情况
SHOW ENGINE INNODB STATUS;

Reference